﻿namespace NFakeMail.UnitTests
{
    using System;
    using System.Diagnostics;
    using System.Moles;
    using System.Reflection;
    using System.Threading;
    using Microsoft.Moles.Framework;
    using Microsoft.Moles.Framework.NUnit;
    using NFakeMail.Moles;
    using NUnit.Framework;

    public sealed class FakeSmtpServerTests
    {
        [Test]
        [Description("SHOULD throw an exception if output sink parameter is a null reference.")]
        public void Constructor_T001()
        {
            var exception = Assert.Throws<ArgumentNullException>(() => new FakeSmtpServer(null, 1025));

            Assert.AreEqual("output", exception.ParamName);
        }

        [Test]
        [Description("SHOULD throw an exception if port parameter is less than zero.")]
        public void Constructor_T002()
        {
            var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new FakeSmtpServer(-1));

            Assert.AreEqual("port", exception.ParamName);
        }

        [Test]
        [Description("SHOULD throw an exception if port parameter is greater than 65535.")]
        public void Constructor_T003()
        {
            var exception = Assert.Throws<ArgumentOutOfRangeException>(() => new FakeSmtpServer(65536));

            Assert.AreEqual("port", exception.ParamName);            
        }

        [Test, Moled]
        [Description("SHOULD throw an exception if IRONPYTHONPATH environment variable is not set.")]
        public void Constructor_T004()
        {
            MEnvironment.GetEnvironmentVariableString = (name) =>
            {
                return string.Empty;
            };

            Assert.Throws<InvalidOperationException>(() => new FakeSmtpServer(10025));
        }

        [Test]
        [Description("SHOULD throw an exception if the instance was already disposed.")]
        public void WaitForMessage_T001()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            server.Dispose();

            Assert.Throws<ObjectDisposedException>(() => server.WaitForMessage(null, 0));
        }

        [Test]
        [Description("SHOULD throw an exception if the timeout parameter is a negative integer.")]
        public void WaitForMessage_T002()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var exception = Assert.Throws<ArgumentOutOfRangeException>(
                () => server.WaitForMessage(null, -1));

            Assert.AreEqual("timeout", exception.ParamName);
        }

        [Test]
        [Description("SHOULD throw an exception if a message is not received in the specified timeout.")]
        public void WaitForMessage_T003()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            Assert.Throws<TimeoutException>(() => server.WaitForMessage(null, 1000));
        }

        [Test]
        [Description("SHOULD wait for a message until at least the specified timeout elapses.")]
        public void WaitForMessage_T004()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var sw = new Stopwatch();

            sw.Start();
            try
            {
                server.WaitForMessage(null, 2000);
            }
            catch (TimeoutException) { }
            sw.Stop();

            Assert.GreaterOrEqual(sw.ElapsedMilliseconds, 2000);
        }

        [Test, Moled]
        [Description("SHOULD ignore messages received with subjects different than the one specified.")]
        public void WaitForMessage_T005()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver", 
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var ignoredMsg = new MFakeMailMessage { SubjectGet = () => "Hello" }.Instance;

            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(ignoredMsg));

            ThreadPool.QueueUserWorkItem(s =>
            {
                Thread.Sleep(500);

                receiver.ReceiveMessage(ignoredMsg);
            });

            var targetMsg = new MFakeMailMessage { SubjectGet = () => "Hello World!" }.Instance;

            ThreadPool.QueueUserWorkItem(s =>
            {
                Thread.Sleep(1000);

                receiver.ReceiveMessage(targetMsg);
            });

            var receivedMsg = server.WaitForMessage("Hello World!", 2000);

            Assert.AreEqual(targetMsg, receivedMsg);
        }

        [Test, Moled]
        [Description("SHOULD ignore casing when waiting for a message with the specified subject.")]
        public void WaitForMessage_T006()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var targetMsg = new MFakeMailMessage { SubjectGet = () => "hello world!" }.Instance;

            ThreadPool.QueueUserWorkItem(s =>
            {
                Thread.Sleep(1000);

                receiver.ReceiveMessage(targetMsg);
            });

            var receivedMsg = server.WaitForMessage("Hello World!", 2000);

            Assert.AreEqual(targetMsg, receivedMsg);
        }

        [Test, Moled]
        [Description("SHOULD wait until any message is received if the " + 
            "specified subject is a null reference.")]
        public void WaitForMessage_T007()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var targetMsg = new MFakeMailMessage { SubjectGet = () => "Hello World!" }.Instance;

            ThreadPool.QueueUserWorkItem(s =>
            {
                Thread.Sleep(250);

                receiver.ReceiveMessage(targetMsg);
            });

            var receivedMsg = server.WaitForMessage(null, 2000);

            Assert.AreEqual(targetMsg, receivedMsg);
        }

        [Test, Moled]
        [Description("SHOULD return the first message from the received messages queue.")]
        public void WaitForMessage_T008()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var firstMsgMole = new MFakeMailMessage { SubjectGet = () => "First" };

            receiver.ReceiveMessage(firstMsgMole.Instance);

            var secondMsgMole = new MFakeMailMessage { SubjectGet = () => "Second" };

            receiver.ReceiveMessage(secondMsgMole.Instance);

            var receivedMsg = server.WaitForMessage();

            Assert.AreEqual(firstMsgMole.Instance, receivedMsg);
        }

        [Test, Moled]
        [Description("SHOULD remove the message from the received messages queue.")]
        public void WaitForMessage_T009()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            receiver.ReceiveMessage(new MFakeMailMessage().Instance);

            server.WaitForMessage();

            Assert.AreEqual(0, server.GetAvailableMessages().Length);
        }

        [Test]
        [Description("SHOULD throw an exception if the instance was already disposed.")]
        public void WaitForMessages_T001()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            server.Dispose();

            Assert.Throws<ObjectDisposedException>(() => server.WaitForMessages(1));
        }

        [Test]
        [Description("SHOULD throw an exception if the count parameter is a negative integer.")]
        public void WaitForMessages_T002()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var exception = Assert.Throws<ArgumentOutOfRangeException>(() => server.WaitForMessages(-1));

            Assert.AreEqual("count", exception.ParamName);
        }

        [Test]
        [Description("SHOULD throw an exception if the count parameter is zero.")]
        public void WaitForMessages_T003()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var exception = Assert.Throws<ArgumentOutOfRangeException>(() => server.WaitForMessages(0));

            Assert.AreEqual("count", exception.ParamName);
        }

        [Test]
        [Description("SHOULD throw an exception if the timeout parameter is a negative integer.")]
        public void WaitForMessages_T004()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var exception = Assert.Throws<ArgumentOutOfRangeException>(() => server.WaitForMessages(1, -1));

            Assert.AreEqual("timeout", exception.ParamName);
        }

        [Test, Moled]
        [Description("SHOULD throw an exception if the specified message count " + 
            "is not reached within the specified timeout.")]
        public void WaitForMessages_T005()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));

            Assert.Throws<TimeoutException>(() => server.WaitForMessages(3, 2000));
        }

        [Test]
        [Description("SHOULD wait for messages until at least the specified timeout elapses.")]
        public void WaitForMessages_T006()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var sw = new Stopwatch();

            sw.Start();
            try
            {
                server.WaitForMessages(3, 2000);
            }
            catch (TimeoutException) { }
            sw.Stop();

            Assert.GreaterOrEqual(sw.ElapsedMilliseconds, 2000);
        }

        [Test, Moled]
        [Description("SHOULD wait for the specified message count.")]
        public void WaitForMessages_T007()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));

            var messages = server.WaitForMessages(3, 2000);

            Assert.AreEqual(3, messages.Length);
        }

        [Test, Moled]
        [Description("SHOULD leave any received message in the queue if " + 
            "timeout elapses before reaching message count.")]
        public void WaitForMessages_T008()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));
            ThreadPool.QueueUserWorkItem(s => receiver.ReceiveMessage(message));

            try
            {
                server.WaitForMessages(5, 2000);
            }
            catch (TimeoutException) { }

            Assert.AreEqual(4, server.GetAvailableMessages().Length);
        }

        [Test]
        [Description("SHOULD throw an exception if instance was already disposed.")]
        public void ClearAvailableMessages_T001()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            server.Dispose();

            Assert.Throws<ObjectDisposedException>(() => server.ClearAvailableMessages());
        }

        [Test, Moled]
        [Description("SHOULD clear the received messages queue.")]
        public void ClearAvailableMessages_T002()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);

            server.ClearAvailableMessages();

            Assert.AreEqual(0, server.GetAvailableMessages().Length);
        }

        [Test]
        [Description("SHOULD throw an exception if the instance was already disposed.")]
        public void GetAvailableMessages_T001()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            server.Dispose();

            Assert.Throws<ObjectDisposedException>(() => server.GetAvailableMessages());
        }

        [Test, Moled]
        [Description("SHOULD retrieve all the messages in the queue.")]
        public void GetAvailableMessages_T002()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);

            var messages = server.GetAvailableMessages();

            Assert.AreEqual(4, messages.Length);
        }

        [Test, Moled]
        [Description("SHOULD clear the messages queue.")]
        public void GetAvailableMessages_T003()
        {
            var server = new FakeSmtpServer(NoopOutputSink.Instance, 10025);

            var property = typeof(FakeSmtpServer).GetProperty(
                "MessageReceiver",
                BindingFlags.NonPublic | BindingFlags.Instance);

            dynamic receiver = property.GetValue(server, BindingFlags.GetProperty, null, null, null);

            var message = new MFakeMailMessage().Instance;

            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);
            receiver.ReceiveMessage(message);

            var messages = server.GetAvailableMessages();

            Assert.AreEqual(0, server.GetAvailableMessages().Length);
        }
    }
}
